/**********************************************************************
 *   Copyright: (C)2024 LingYun IoT System Studio
 *      Author: LiJiahui<2199250859@qq.com>
 *
 * Description: The purpose of this code is to provide a simple C library,
 *              which providing software bit-bang of the I2C protocol on
 *              any GPIO pins for ISKBoard.
 *
 *   ChangeLog:
 *        Version    Date       Author            Description
 *        V1.0.0  2024.08.29    LiJiahui      Release initial version
 *
 ***********************************************************************/

#include <stdio.h>
#include <string.h>

#include "main.h"
#include "w25q.h"
#include "spi.h"


#define CONFIG_DEBUG_W25Q /* Enable W25Q norflash debug */


#ifdef CONFIG_DEBUG_W25Q
#define spinor_print(format, args...) printf(format, ##args)
#else
#define spinor_print(format, args...) do{} while(0)
#endif


/* __attribute__((unused)) 是一个gcc编译器属性，表示该函数可能未被使用，防止编译器产生“未使用函数”的警告 */
__attribute__((unused)) static void dump_buf(const char *prompt, uint8_t *buf, uint32_t size)
{
	int	i;

	if( !buf )
		return ;

	if( prompt )
		printf("%s\r\n", prompt);

	for(i=0; i<size; i++)
		printf("%02x ", buf[i]);

	printf("\r\n");
}


#ifdef USE_FREERTOS
#include "cmsis_os.h"    // 包含FreeRTOS的CMSIS-OS接口头文件
#define mdelay(ms) osDelay(ms)  // 使用FreeRTOS的延时函数
#else
#define mdelay(ms) HAL_Delay(ms) // 使用HAL库的延时函数
#endif


/* SPI API functions */

/* 为了保证可移植性，此处选择软件NSS */
#define W25Q_SPI		&hspi1
#define W25Q_CS_PORT	GPIOA
#define W25Q_CS_PIN		GPIO_PIN_4

#define SPI_DUMMY_BYTE	0xA5

void spinor_gpio_init(struct spi_info *spi)
{
	GPIO_InitTypeDef GPIO_InitStruct = {0};

	/* Initial W25Q Norflash SPI CS pin */
	HAL_GPIO_WritePin(spi->cs_gpio, spi->cs_pin, GPIO_PIN_SET); /* 将CS设为高电平，默认不选中Flash芯片 */
	GPIO_InitStruct.Mode = GPIO_MODE_OUTPUT_PP;
	GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_HIGH;
	GPIO_InitStruct.Pin = spi->cs_pin;
	HAL_GPIO_Init(spi->cs_gpio, &GPIO_InitStruct);
}

void spi_cs_enable(struct spi_info *spi)
{
	HAL_GPIO_WritePin(spi->cs_gpio, spi->cs_pin, GPIO_PIN_RESET);
}

void spi_cs_disable(struct spi_info *spi)
{
	HAL_GPIO_WritePin(spi->cs_gpio, spi->cs_pin, GPIO_PIN_SET);
}

/* SPI 单字节收发函数，通过 SPI 总线同时发送和接收1字节数据 */
uint8_t spi_xfer(struct spi_info *spi, uint8_t data)
{
	uint8_t	rxbyte; /* 存储接收到的字节 */
	HAL_SPI_TransmitReceive(spi->hspi, &data, &rxbyte, 1, 100);
	return rxbyte;
}

void spi_send(struct spi_info *spi, uint8_t *data, uint32_t bytes)
{
	HAL_SPI_Transmit(spi->hspi, data, bytes, 100);
}

void spi_recv(struct spi_info *spi, uint8_t *buf, uint32_t size)
{
	HAL_SPI_Receive(spi->hspi, buf, size, 100);
}

/* 此处定义了一个宏，用来快速初始化SPI */
#define SPI_INFO(_hspi, _cs_gpio, _cs_pin){\
	.hspi		= _hspi,			\
	.cs_gpio	= _cs_gpio,			\
	.cs_pin		= _cs_pin,			\
	.select		= spi_cs_enable,	\
	.deselect	= spi_cs_disable,	\
	.xfer		= spi_xfer,			\
	.send		= spi_send,			\
	.recv		= spi_recv,			\
}

/* 这里通过 SPI_INFO 宏将各种函数赋值给了结构体的函数指针成员，完成了 SPI 接口的底层实现 */
static struct spi_info spinor_spi = SPI_INFO(W25Q_SPI, W25Q_CS_PORT, W25Q_CS_PIN);


/* W25Q SPI Norflash ID */

#define W25Q_PAGSIZE	256 /* 每页大小为256字节 */
#define W25Q_SECSIZE	4096 /* 每个扇区大小为4KB（16页） */
#define W25Q_BLKSIZE	65536 /* 每个块大小为64KB（16扇区） */

#define ARRAY_SIZE(x)		(sizeof(x) / sizeof(x[0]))

/* JEDEC ID 的第3个字节（最低字节）编码了该闪存芯片的存储容量信息 */

/* GCC编译器的诊断指令，用于临时禁用特定的编译器警告
 * 作用：忽略“移位计数溢出” 的警告，当进行左移操作时，如果移位位数超过了该数据类型的位数，GCC通常会发出警告
 */
#pragma GCC diagnostic ignored "-Wshift-count-overflow"

/* 从芯片的 JEDEC ID 中提取容量信息，取JEDEC ID的最低字节，将数字1左移(id & 0xFF)位
 * UL：确保是unsigned long类型，避免移位时的符号问题和位数限制
 */
#define CAPCITY_ID(id)			(1UL << ( id & 0xFF ))

/* 快速填充Flash芯片的信息：型号名称、JEDEC ID、存储单元大小（块，扇区，页）、总容量（字节）、块/扇区/页的数量
 * 此处不加{}是因为在下面的结构体结构体初始化时已经有了{}，这里再加{}会嵌套，故报错
*/

#define	NOR_INFO(_name,_jedec_id)	\
	.name			= _name,								\
	.jedec_id		= _jedec_id,							\
	.block_size		= W25Q_BLKSIZE,							\
	.sector_size	= W25Q_SECSIZE,							\
	.page_size		= W25Q_PAGSIZE,							\
	.capacity		= CAPCITY_ID(_jedec_id),				\
	.n_blocks		= CAPCITY_ID(_jedec_id)/W25Q_BLKSIZE,	\
	.n_sectors		= CAPCITY_ID(_jedec_id)/W25Q_SECSIZE,	\
	.n_pages		= CAPCITY_ID(_jedec_id)/W25Q_PAGSIZE,	\


/* 批量定义了一系列W25Q系列 SPI NOR FLASH 芯片的型号及其参数 */
static struct flash_info spinor_ids[] = {
        { NOR_INFO("W25Q512", 0xef4020) },
        { NOR_INFO("W25Q256", 0xef4019) },
        { NOR_INFO("W25Q128", 0xef4018) },
        { NOR_INFO("W25Q64",  0xef4017) },
        { NOR_INFO("W25Q32",  0xef4016) },
        { NOR_INFO("W25Q16",  0xef4015) },
        { NOR_INFO("W25Q80",  0xef4014) },
        { NOR_INFO("W25Q40",  0xef4013) },
        { NOR_INFO("W25Q20",  0xef4012) },
        { NOR_INFO("W25Q10",  0xef4011) },
};

/* SPI Norflash Highlevel API */

/* SPI Norflash API test function */
void spinor_test(void)
{
	spinor_info_t	spinor;
	int				i;
	uint8_t			buf[W25Q_PAGSIZE*2];

	if( spinor_init(&spinor) < 0 )
		return ;

	/* 此处选择扇区擦除：此处测试场景仅需验证局部数据的擦除和读写，因此扇区擦除是最合理的选择
	 * 而块擦除和片擦除因操作范围过大或风险过高此处不适用
	*/
	//spinor_erase_chip(&spinor);
	//spinor_erase_block(&spinor, 1, W25Q_BLKSIZE);
	spinor_erase_sector(&spinor, 1, W25Q_SECSIZE);

	memset(buf, 0 ,sizeof(buf));
	spinor_read(&spinor, 0, buf, sizeof(buf));
	dump_buf("<<<Read data after erase:\n", buf, sizeof(buf));

	/* 非页对齐地址读写测试 */
	for(i=0; i<sizeof(buf); i++)
		buf[i] = i;
	spinor_write(&spinor, 8, buf, W25Q_PAGSIZE);

	memset(buf, 0, sizeof(buf));
	spinor_read(&spinor, 0, buf, sizeof(buf));
	dump_buf("<<<Read data after write:\n", buf, sizeof(buf));

	return ;
}

/* SPI NOR Flash 初始化函数，初始化SPI接口并检测连接的Flash芯片型号 */
int spinor_init(struct spinor_info *spinor)
{
	spinor->spi = &spinor_spi;

	spinor_gpio_init(spinor->spi);

	if( !spinor_detect_by_jedec(spinor) )
		return -1;

	printf("Norflash %s detected, capacity %lu KB, %lu blocks, %lu sectors, %lu pages\r\n",
			spinor->flash->name, spinor->flash->capacity>>10,
			spinor->flash->n_blocks, spinor->flash->n_sectors, spinor->flash->n_pages);

	return 0;
}

/* 该函数用于SPI NOR Flash 的整片（页）擦除，将整个 Flash 芯片的所有存储单元重置为0xFF
 * Refence:	p59 8.2.32 Chip Erase(C7h / 60h)
 */
int spinor_erase_chip(struct spinor_info *spinor)
{
	struct spi_info *spi = spinor->spi;

	while(spinor->lock == 1)
		mdelay(1);

	spinor->lock = 1;

#ifdef CONFIG_DEBUG_W25Q
	uint32_t StartTime = HAL_GetTick();
	printf("Norflash EraseChip Begin...\r\n");
#endif

	spinor_write_enable(spi);

	spi->select(spi);
	spi->xfer(spi, SPINOR_OP_CHIP_ERASE);
	spi->deselect(spi);
	spinor_WaitForWriteEnd(spi);

#ifdef CONFIG_DEBUG_W25Q
	printf("Norflash EraseChip done after %ld ms!\r\n", HAL_GetTick() - StartTime);/* 获取擦除扇区所需时间 */
#endif

	mdelay(1);
	spinor->lock = 0;

	return 0;
}

/* 该函数用于SPI NOR Flash 的块擦除，按64KB块为单位擦除指定范围的数据
 * Reference:W25Q256JV datasheet P58 8.2.31 64KB Block Erase with 4-Byte Address (DCh)
 * address和size与下面的扇区擦除意义相同
 */
int spinor_erase_block(struct spinor_info *spinor, uint32_t address, uint32_t size)
{
	struct spi_info 	*spi = spinor->spi;
	struct flash_info 	*flash = spinor->flash;
	uint32_t			block, first, last;
	uint32_t			addr;

	while(spinor->lock == 1)
		mdelay(1);

	spinor->lock = 1;

	first = address / flash->block_size;
	last = (address+size-1) / flash->block_size;

#ifdef CONFIG_DEBUG_W25Q
	uint32_t StartTime = HAL_GetTick();
	printf("Norflash Erase %ld Bytes Block@0x%lx Begin...\r\n", size, address);
#endif

	/* Start erase all the blocks */
	for(block = first; block <= last; block++)
	{
		addr = block * flash->sector_size;

#ifdef CONFIG_DEBUG_W25Q
		printf("Norflash Earse Block@%lx...\r\n", addr);
#endif

		spinor_WaitForWriteEnd(spi);
		spinor_write_enable(spi);

		spi->select(spi);
		if(spinor->flash->n_blocks >= 512) /* larger than W25Q256 */
		{
			spi->xfer(spi, SPINOR_OP_BE_4K_4B);
			spi->xfer(spi, (addr & 0xFF000000) >> 24);
		}
		else
		{
			spi->xfer(spi, SPINOR_OP_BE_4K);
		}

		spi->xfer(spi, (address & 0xFF0000) >> 16);
		spi->xfer(spi, (address & 0xFF00) >> 8);
		spi->xfer(spi, (address & 0xFF));
		spi->deselect(spi);

		spinor_WaitForWriteEnd(spi);
	}

#ifdef CONFIG_DEBUG_W25Q
	printf("Norflash EraseSector@0x%lx done after %ld ms\r\n", address, HAL_GetTick() - StartTime);/* 获取擦除扇区所需时间 */
	mdelay(100);
#endif

	mdelay(1);
	spinor->lock = 0;

	return 0;
}



/* 该函数用于擦除Flash存储器中以4KiB为最小单位的扇区
 * Reference:W25Q256JV datasheet P55 8.2.28 Sector Erase with 4-Byte Address (21h)
 *  address:擦除操作的起始物理地址，可以是非对齐地址，及时地址不是4KiB对齐的（如0x1001），函数内部会自动向下对其到最近的4KiB边界（即0x1000）
 *  size:要擦除的字节数，可以超过一个扇区的大小，如果size超过一个扇区，函数会擦除所有跨越的扇区，此处擦除两个扇区
 */
int spinor_erase_sector(struct spinor_info *spinor, uint32_t address, uint32_t size)
{
	struct spi_info 	*spi = spinor->spi;
	struct flash_info	*flash = spinor->flash;
	uint32_t			sector, first, last;
	uint32_t			addr;

	/* 等待锁释放（使用spinor->lock实现互斥锁，确保同一时间只有一个任务能擦除Flash，避免数据冲突） */
	while(spinor->lock == 1)
		mdelay(1);

	spinor->lock = 1; /* 加锁，防止多任务并发操作 */

	/* 计算擦除范围 */
	first = address / flash->sector_size; /* 起始扇区号 */
	last = (address+size-1) / flash->sector_size; /* 结束扇区号 */

#ifdef CONFIG_DEBUG_W25Q
	uint32_t StartTime = HAL_GetTick();
	printf("Norflash Erase %ld Bytes Sector@0x%lx Begin...\r\n", size, address);
#endif

	/* Start erase all the sectors */
	for(sector = first; sector<=last; sector++)
	{
		addr = sector * flash->sector_size; /* 扇区起始地址（实现自动对齐）*/

#ifdef CONFIG_DEBUG_W25Q
		printf("Norflash Earse Sector@%lx...\r\n", addr);
#endif

		/* 等待flash空闲，当连续擦除多个扇区时，后续命令可能在前一个扇区擦除完成前发送，导致操作失败,同步MCU与Flash的操作时序 */
		spinor_WaitForWriteEnd(spi);

		/* 写使能，在 SPI NOR Flash 中，所有写操作（擦除、编程）前必须先使能写状态（WEL位） */
		spinor_write_enable(spi);

		/* 发送扇区擦除命令 */
		spi->select(spi);
		if(spinor->flash->n_blocks >= 512) /* 4字节地址模式（大容量芯片） */
		{
			spi->xfer(spi, SPINOR_OP_SE_4B); /* 发送4字节扇区擦除命令 */
			spi->xfer(spi, (addr & 0xFF000000) >>24); /* 先发送最高字节地址 */
		}
		else
		{
			spi->xfer(spi, SPINOR_OP_SE);
		}
		spi->xfer(spi, (addr & 0xFF0000) >>16); /* 发送高字节 */
		spi->xfer(spi, (addr & 0xFF00) >>8);	/* 发送中字节 */
		spi->xfer(spi, (addr & 0xFF) >>16);		/* 发送低字节 */
		spi->deselect(spi);

		spinor_WaitForWriteEnd(spi); /* 等待当前扇区擦除完成 */
	}

#ifdef CONFIG_DEBUG_W25Q
	printf("Norflash EraseSector@0x%lx done after %ld ms\r\n", address, HAL_GetTick() - StartTime);/* 获取擦除扇区所需时间 */
#endif

	mdelay(1);
	spinor->lock = 0;

	return 0;
}

/* P50 8.2.23 Page Program(02h)
 * 该函数实现了页写入功能，支持非对齐地址和跨页写入
 * 限制：1、单次写入不能超过页大小（256字节）；2、写入前必须确保目标扇区已被擦除（全FF）
*/
int spinor_write(struct spinor_info *spinor, uint32_t address, uint8_t *data, uint32_t bytes)
{
	struct spi_info 	*spi = spinor->spi;
	struct flash_info 	*flash = spinor->flash;
	uint32_t			page, first, last;
	uint32_t			addr, ofset, len;

	while(spinor->lock == 1)
		mdelay(1);

	spinor->lock = 1;

	/* find first and last write page
	 * 例：若address = 0x0008, byte=256,页大小256字节：
	 * first = 0x0008 / 256 = 0(第0页)
	 * last = (0x0008+256-1) / 256 = 1(第1页)
	 * 表明数据会跨两个页
	*/
	first = address / flash->page_size;
	last = (address+bytes-1) / flash->page_size;

#ifdef CONFIG_DEBUG_W25Q
	uint32_t StartTime = HAL_GetTick();
	printf("Norflash Write %ld Bytes to addr@0x%lx Begin...\r\n", bytes, address);
#endif

	/* address in page and offset in buffer */
	addr = address;
	ofset = 0;

	/* start write all the pages */
	for(page=first; page<=last; page++)
	{
		len = flash->page_size - (addr%flash->page_size); /* 当前页剩余空间 */
		len = len > bytes ? bytes : len; /* 取较小值，避免溢出 */

#ifdef CONFIG_DEBUG_W25Q
		printf("Norflash write addr@0x%lx, %lu bytes\r\n", addr, len);
#endif

		spinor_WaitForWriteEnd(spi); /* 等待上次写入完成 */
		spinor_write_enable(spi);

		spi->select(spi);
		if(spinor->flash->n_blocks >= 512)
		{
			spi->xfer(spi, SPINOR_OP_PP_4B);
			spi->xfer(spi, (address & 0xFF000000) >> 24);
		}
		else
		{
			spi->xfer(spi, SPINOR_OP_PP);
		}

		spi->xfer(spi, (address & 0xFF0000) >> 16);
		spi->xfer(spi, (address & 0xFF00) >> 8);
		spi->xfer(spi, (address & 0xFF));

		/* send data */
		spi->send(spi, data+ofset, len);

		spi->deselect(spi);
		spinor_WaitForWriteEnd(spi);

		addr += len; /* 更新写入地址 */
		ofset += len; /* 更新数据缓冲区偏移量 */
		bytes -= len; /* 更新剩余字节数 */
	}

#ifdef CONFIG_DEBUG_W25Q
	printf("Norflash WriteByte@0x%lx done after %ld ms\r\n", address, HAL_GetTick() - StartTime);/* 获取擦除扇区所需时间 */
#endif

	mdelay(1);
	spinor->lock = 0;

	return 0;

}


/* 该函数使用 SPI Flash的快速读取指令，可读取整个芯片的任意区域
 * 快速读取模式（如命令码0x0B/0x0C）通过插入哑字节（Dummy Byte）提高时钟频率，比标准读取（如0x03）更快，适合连续大量数据读取
 * Reference  :  P40, 8.2.13 Fast Read with 4-Byte Address (0Ch)
 * address:读取操作的起始物理地址，可以是非对齐地址，及时地址不是对齐的（如0x1001），函数内部会自动向下对其到最近的边界
 * @bytes参数是读取的字节数，可超过页大小（如257字节会读取2页）
 * 快速读取支持突发模式（Burst Mode），一次命令可连续读取任意长度数据，无需多次片选（CS）切换
 */
int spinor_read(struct spinor_info *spinor, uint32_t address, uint8_t *buf, uint32_t bytes)
{
	struct spi_info *spi = spinor->spi;

	while( spinor->lock == 1 )
		mdelay(1);

	spinor->lock = 1;

#ifdef CONFIG_DEBUG_W25Q
	uint32_t StartTime = HAL_GetTick();
	printf("Norflash Read %ld Bytes from addr@0x%lx Begin...\r\n", bytes, address);
#endif

	spi->select(spi);

	/* send instruction and address */
	if(spinor->flash->n_blocks >= 512)
	{
		spi->xfer(spi, SPINOR_OP_READ_FAST_4B);
		spi->xfer(spi, (address & 0xFF000000)  >> 24);
	}
	else
	{
		spi->xfer(spi, SPINOR_OP_READ_FAST);
	}

	spi->xfer(spi, (address & 0xFF0000) >> 16);
	spi->xfer(spi, (address & 0xFF00) >> 8);
	spi->xfer(spi, (address & 0xFF));

	/* read data */
	spi->xfer(spi, SPI_DUMMY_BYTE);
	spi->recv(spi, buf, bytes);

	spi->deselect(spi);

#ifdef CONFIG_DEBUG_W25Q
	printf("Norflash ReadBYtes@0x%lx done after %ld ms\r\n", address, HAL_GetTick() - StartTime);/* 获取擦除扇区所需时间 */
#endif

	spinor->lock = 0;

	return 0;
}


/* ----------------------------------
 *     SPI Norflash LowLevel API    /
 -----------------------------------*/

/* Detect the norflash by JEDEC ID(检测连接的SPI NOR Flash 芯片型号) */
int spinor_detect_by_jedec(struct spinor_info *spinor)
{
	uint32_t	jedec_id;
	int			i, found = 0;

	jedec_id = spinor_read_jedecid(spinor->spi);

	/* ARRAY_SIZE(spinor_ids)计算共有几种芯片，通过遍历确定芯片型号 */
	for(i=0; i<ARRAY_SIZE(spinor_ids); i++)
	{
		if(spinor_ids[i].jedec_id == jedec_id)
		{
			found = 1;  /* 检测到匹配的芯片型号 */
			spinor->flash = &spinor_ids[i];
			break;
		}

	}

	printf("Detect JEDEC ID[0x%lx], Norflash %s found\r\n", jedec_id, found?spinor->flash->name:"not");
	return found;
}


/*
 * 实现了SPI NOR Flash 芯片的 JEDEC ID 读取功能，通过SPI 总线发送标准命令（0x9F）并解析返回的 3 字节标识符
 * JEDEC ID 有三字节：高八位：厂商ID， 中间八位：设备类型 低八位：容量代码
*/
uint32_t spinor_read_jedecid(struct spi_info *spi)
{
	uint32_t	jedec_id = 0x0;
	uint8_t		id[3];

	spi->select(spi); /* 选中该w25q芯片（cs引脚拉低） */
	spi->xfer(spi, SPINOR_OP_RDID); /* 发送“读取 JEDEC ID 命令” */

	/* 此处SPI_DUMMY_BYTE是填充数据，无关紧要 */
	id[0] = spi->xfer(spi, SPI_DUMMY_BYTE); /* 读取厂商ID */
	id[1] = spi->xfer(spi, SPI_DUMMY_BYTE); /* 读取设备类型 */
	id[2] = spi->xfer(spi, SPI_DUMMY_BYTE); /* 读取容量代码 */
	spi->deselect(spi); /* 释放芯片（cs引脚拉高） */

	jedec_id = (id[0] << 16) | (id[1] << 8) |id[2];
	return jedec_id;
}

/* Description:  Write Enable
 * Reference  :  P30, 8.2.1 Write Enable (06h)
 */
void spinor_write_enable(struct spi_info *spi)
{
	spi->select(spi);
	spi->xfer(spi, SPINOR_OP_WREN);
	spi->deselect(spi);

	mdelay(1);
}


/* 循环读取Flash的状态寄存器1（SR1:Flash芯片的核心控制寄存器），直到写操作（包括编程、擦除）完成
 * Reference:P14 7.1 Status Registers
 */
void spinor_WaitForWriteEnd(struct spi_info *spi)
{
	uint8_t value = 0;
	mdelay(1); /* 给Flash留出初始响应时间，避免立即读取状态寄存器导致的不稳定 */

	spi->select(spi);
	spi->xfer(spi, SPINOR_OP_RDSR1); /* 发送读状态寄存器1命令 */

	/* 循环读取BUSY位（Bit 0） */
	do
	{
		value = spi->xfer(spi, SPI_DUMMY_BYTE); /* 发送哑字节，接收状态寄存器值 */
		mdelay(1);
	}while((value & 0x01) == 0x01); /* 当BUSY位为1时继续循环 */

	spi->deselect(spi); /* 拉高CS引脚，结束SPI通信 */
}
